package com.linkoog.devtools.utils;

import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingProcessHandler;
import com.intellij.execution.process.ProcessIOExecutorService;
import com.intellij.execution.util.ExecUtil;
import com.intellij.ide.IdeBundle;
import com.intellij.idea.ActionsBundle;
import com.intellij.jna.JnaLoader;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.MessageDialogBuilder;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.NlsActions;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Ole32;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;

public class ExplorerHolder {

    private static final Logger LOG = Logger.getInstance(ExplorerHolder.class);

    private static final String[] supportedFileManagers = {"nautilus", "pantheon-files", "dolphin", "dde-file-manager"};

    public static final @Nullable String fileManagerApp;
    public static final @Nullable @NlsSafe String fileManagerName;

    static {
        String fmApp = null, fmName = null;
        if (SystemInfo.hasXdgMime()) {
            var desktopEntryName = ExecUtil.execAndReadLine(new GeneralCommandLine("xdg-mime", "query", "default", "inode/directory"));
            if (desktopEntryName != null && desktopEntryName.endsWith(".desktop")) {
                var desktopFile = Stream.of(getXdgDataDirectories().split(":"))
                        .map(dir -> Path.of(dir, "applications", desktopEntryName))
                        .filter(Files::exists)
                        .findFirst();
                if (desktopFile.isPresent()) {
                    try {
                        var lines = Files.readAllLines(desktopFile.get());
                        fmApp = lines.stream()
                                .filter(line -> line.startsWith("Exec="))
                                .map(line -> line.substring(5).split(" ")[0])
                                .filter(app -> ContainerUtil.exists(supportedFileManagers, supportedFileManager -> app.endsWith(supportedFileManager)))
                                .findFirst().orElse(null);
                        fmName = lines.stream()
                                .filter(line -> line.startsWith("Name="))
                                .map(line -> line.substring(5))
                                .findFirst().orElse(null);
                    }
                    catch (InvalidPathException | IOException e) {
                        LOG.error(e);
                    }
                }
            }
        }
        fileManagerApp = fmApp;
        fileManagerName = fmName;
    }

    private static String getXdgDataDirectories() {
        return StringUtil.defaultIfEmpty(System.getenv("XDG_DATA_HOME"), SystemProperties.getUserHome() + "/.local/share") + ':' +
                StringUtil.defaultIfEmpty(System.getenv("XDG_DATA_DIRS"), "/usr/local/share:/usr/share");
    }



    /** @see #openFile(Path) */
    public static void openFile(@NotNull File file) {
        openFile(file.toPath());
    }

    /**
     * Opens a system file manager with the given file's parent directory loaded and the file highlighted in it
     * (note that some platforms do not support the file highlighting).
     */
    public static void openFile(@NotNull Path file) {
        Path parent = file.toAbsolutePath().getParent();
        if (parent != null) {
            doOpen(parent, file);
        }
        else {
            doOpen(file, null);
        }
    }

    /** @see #openDirectory(Path) */
    public static void openDirectory(@NotNull File directory) {
        doOpen(directory.toPath(), null);
    }

    /**
     * Opens a system file manager with the given directory loaded in it.
     */
    public static void openDirectory(@NotNull Path directory) {
        doOpen(directory, null);
    }

    private static void doOpen(@NotNull Path _dir, @Nullable Path _toSelect) {
        String dir = _dir.toAbsolutePath().normalize().toString();
        String toSelect = _toSelect != null ? _toSelect.toAbsolutePath().normalize().toString() : null;
        String fmApp;

        if (SystemInfo.isWindows) {
            if (JnaLoader.isLoaded()) {
                openViaShellApi(dir, toSelect);
            }
            else {
                spawn(toSelect != null ? "explorer /select,\"" + toSelect + '"' : "explorer /root,\"" + dir + '"');
            }
        }
        else if (SystemInfo.isMac) {
            if (toSelect != null) {
                spawn("open", "-R", toSelect);
            }
            else {
                spawn("open", dir);
            }
        }
        else if ((fmApp = fileManagerApp) != null) {
            if (toSelect != null && fmApp.endsWith("dolphin")) {
                spawn(fmApp, "--select", toSelect);
            }
            else if (toSelect != null && fmApp.endsWith("dde-file-manager")) {
                spawn(fmApp, "--show-item", toSelect);
            }
            else {
                spawn(fmApp, toSelect != null ? toSelect : dir);
            }
        }
        else if (SystemInfo.hasXdgOpen()) {
            spawn("xdg-open", dir);
        }
        else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
            if (LOG.isDebugEnabled()) LOG.debug("opening " + dir + " via Desktop API");
            ProcessIOExecutorService.INSTANCE.execute(() -> {
                try {
                    Desktop.getDesktop().open(new File(dir));
                }
                catch (Exception e) {
                    LOG.warn(e);
                }
            });
        }
        else {
            Messages.showErrorDialog(IdeBundle.message("message.this.action.isn.t.supported.on.the.current.platform"),
                    IdeBundle.message("dialog.title.cannot.open.file"));
        }
    }

    private static void openViaShellApi(String dir, String toSelect) {
        if (LOG.isDebugEnabled()) LOG.debug("shell open: dir=" + dir + " toSelect=" + toSelect);

        ProcessIOExecutorService.INSTANCE.execute(() -> {
            Ole32.INSTANCE.CoInitializeEx(null, Ole32.COINIT_APARTMENTTHREADED);

            Pointer pIdl = Shell32Ex.INSTANCE.ILCreateFromPath(dir);
            Pointer[] apIdl = toSelect != null ? new Pointer[]{Shell32Ex.INSTANCE.ILCreateFromPath(toSelect)} : null;
            WinDef.UINT cIdl = new WinDef.UINT(apIdl != null ? apIdl.length : 0);
            try {
                WinNT.HRESULT result = Shell32Ex.INSTANCE.SHOpenFolderAndSelectItems(pIdl, cIdl, apIdl, new WinDef.DWORD(0));
                if (!WinError.S_OK.equals(result)) {
                    LOG.warn("SHOpenFolderAndSelectItems(" + dir + ',' + toSelect + "): 0x" + Integer.toHexString(result.intValue()));
                }
            }
            finally {
                if (apIdl != null) {
                    Shell32Ex.INSTANCE.ILFree(apIdl[0]);
                }
                Shell32Ex.INSTANCE.ILFree(pIdl);
            }
        });
    }

    private interface Shell32Ex extends StdCallLibrary {
        Shell32Ex INSTANCE = Native.load("shell32", Shell32Ex.class, W32APIOptions.DEFAULT_OPTIONS);

        Pointer ILCreateFromPath(String path);
        void ILFree(Pointer pIdl);
        WinNT.HRESULT SHOpenFolderAndSelectItems(Pointer pIdlFolder, WinDef.UINT cIdl, Pointer[] apIdl, WinDef.DWORD dwFlags);
    }

    private static void spawn(String... command) {
        if (LOG.isDebugEnabled()) LOG.debug(Arrays.toString(command));

        ProcessIOExecutorService.INSTANCE.execute(() -> {
            try {
                CapturingProcessHandler handler;
                if (SystemInfo.isWindows) {
                    assert command.length == 1 : Arrays.toString(command);
                    Process process = Runtime.getRuntime().exec(command[0]);  // no quoting/escaping is needed
                    handler = new CapturingProcessHandler.Silent(process, null, command[0]);
                }
                else {
                    handler = new CapturingProcessHandler.Silent(new GeneralCommandLine(command));
                }
                handler.runProcess(10000, false).checkSuccess(LOG);
            }
            catch (Exception e) {
                LOG.warn(e);
            }
        });
    }



    //<editor-fold desc="Deprecated stuff.">
    /** @deprecated trivial to implement, just inline */
    @Deprecated(forRemoval = true)
    public static void showDialog(Project project,
                                  @NlsContexts.DialogMessage String message,
                                  @NlsContexts.DialogTitle String title,
                                  @NotNull File file,
                                  @SuppressWarnings("removal") @Nullable DialogWrapper.DoNotAskOption option) {
        if (MessageDialogBuilder.okCancel(title, message)
                .yesText(getActionName(null))
                .noText(IdeBundle.message("action.close"))
                .icon(Messages.getInformationIcon())
                .doNotAsk(option)
                .ask(project)) {
            openFile(file);
        }
    }
    //</editor-fold>

    private static @NlsActions.ActionText @NotNull String getActionName(@Nullable String place) {
        if (ActionPlaces.EDITOR_TAB_POPUP.equals(place) || ActionPlaces.EDITOR_POPUP.equals(place) || ActionPlaces.PROJECT_VIEW_POPUP.equals(place)) {
            return getFileManagerName();
        }
        else if (SystemInfo.isMac) {
            return ActionsBundle.message("action.RevealIn.name.mac");
        }
        else {
            return ActionsBundle.message("action.RevealIn.name.other", getFileManagerName());
        }
    }

    private static @NotNull @NlsActions.ActionText String getFileManagerName() {
        return SystemInfo.isMac ? IdeBundle.message("action.finder.text") :
                SystemInfo.isWindows ? IdeBundle.message("action.explorer.text") :
                        Objects.requireNonNullElseGet(fileManagerName, () -> IdeBundle.message("action.file.manager.text"));
    }
}
